export var ReadWriteConcernDefaultsPropagation = (function() {
    const kDefaultReadConcernField = "defaultReadConcern";
    const kDefaultWriteConcernField = "defaultWriteConcern";
    const kUpdateOpTimeField = "updateOpTime";
    const kUpdateWallClockTimeField = "updateWallClockTime";
    const kLocalUpdateWallClockTimeField = "localUpdateWallClockTime";

    const kDefaultRWCFields = [kDefaultReadConcernField, kDefaultWriteConcernField];
    const kExtraSetFields = [kUpdateOpTimeField, kUpdateWallClockTimeField];
    const kExtraLocalFields = [kLocalUpdateWallClockTimeField];
    const kExtraFields = [...kExtraSetFields, ...kExtraLocalFields];
    const kSetFields = [...kDefaultRWCFields, ...kExtraSetFields];

    const timeoutSecs = 2 * 60;
    const intervalSecs = 5;

    // Check that setting the defaults on setConn propagates correctly across checkConns.
    function setDefaultsAndVerifyPropagation(setConn, checkConns, inMemory) {
        // Get the current defaults from setConn.
        var initialSetConnDefaults =
            assert.commandWorked(setConn.adminCommand({getDefaultRWConcern: 1}));

        // Ensure that all checkConns agree with this. Use a loop in case the initial defaults were
        // recently set and have not yet propagated to all nodes.
        var checkConnsDefaults = [];
        var initialCheckConnsDefaults = [];
        assert.soon(
            () => {
                initialCheckConnsDefaults = checkConns.map(
                    conn => assert.commandWorked(conn.adminCommand({getDefaultRWConcern: 1})));
                return initialCheckConnsDefaults.every(
                    checkConnDefaults =>
                        kSetFields.every(field => friendlyEqual(checkConnDefaults[field],
                                                                initialSetConnDefaults[field])));
            },
            () => "expected initial defaults to be present on all nodes within" + timeoutSecs +
                " secs.  Expected defaults: " + tojson(initialSetConnDefaults) + ", checkConns: " +
                tojson(checkConns) + ", current state: " + tojson(initialCheckConnsDefaults),
            timeoutSecs * 1000,
            intervalSecs * 1000,
            {runHangAnalyzer: false});

        // Set new defaults on setConn.
        var newDefaults = {
            defaultReadConcern: {level: "majority"},
            defaultWriteConcern: {w: 2, wtimeout: 0}
        };
        // If these happen to match what's already there, adjust them.
        if (initialSetConnDefaults.defaultReadConcern &&
            friendlyEqual(initialSetConnDefaults.defaultReadConcern,
                          newDefaults.defaultReadConcern)) {
            newDefaults.defaultReadConcern.level = "local";
        }
        if (initialSetConnDefaults.defaultWriteConcern &&
            friendlyEqual(initialSetConnDefaults.defaultWriteConcern,
                          newDefaults.defaultWriteConcern)) {
            newDefaults.defaultWriteConcern.w++;
        }
        var cmd = {setDefaultRWConcern: 1};
        Object.extend(cmd, newDefaults);
        cmd.writeConcern = {
            w: 1
        };  // Prevent any existing default WC (which may be unsatisfiable) from being applied.
        var newDefaultsRes = assert.commandWorked(setConn.adminCommand(cmd));
        kDefaultRWCFields.forEach(field => assert.eq(newDefaultsRes[field],
                                                     newDefaults[field],
                                                     field + " was not set correctly"));
        assert.hasFields(
            newDefaultsRes, kExtraFields, "missing field in result of setDefaultRWConcern");

        // Check that updateOpTime has increased.  Since everything is running on one host, we can
        // also check that each of kUpdateOpTime and localUpdateWallClockTime have increased.
        kExtraFields.forEach(field => {
            if (field in initialSetConnDefaults) {
                assert.gt(newDefaultsRes[field],
                          initialSetConnDefaults[field],
                          field + " did not increase after setting new defaults");
            }
        });

        // Ensure that all checkConns agree with this.
        assert.soon(
            function() {
                // Get the defaults from all the connections.
                checkConnsDefaults = checkConns.map(conn => assert.commandWorked(conn.adminCommand(
                                                        {getDefaultRWConcern: 1, inMemory})));

                // Check if they all match the recently-set values.
                for (var connDefault of checkConnsDefaults) {
                    if (inMemory) {
                        assert.eq(true, connDefault.inMemory, tojson(connDefault));
                    } else {
                        assert.eq(undefined, connDefault.inMemory, tojson(connDefault));
                    }

                    for (var field of kSetFields) {
                        if (!friendlyEqual(connDefault[field], newDefaultsRes[field])) {
                            return false;
                        }
                    }
                }
                return true;
            },
            () => "updated defaults failed to propagate to all nodes within " + timeoutSecs +
                " secs.  Expected defaults: " + tojson(newDefaults) +
                ", checkConns: " + tojson(checkConns) +
                ", current state: " + tojson(checkConnsDefaults) + ", inMemory: " + inMemory,
            timeoutSecs * 1000,
            intervalSecs * 1000,
            {runHangAnalyzer: false});
    }

    /**
     * Tests propagation of RWC defaults across an environment.  Updated RWC defaults are set using
     * setConn, and then each connection in the checkConns array is checked to ensure that it
     * becomes aware of the new defaults (within the acceptable window of 2 minutes).
     */
    var runTests = function(setConn, checkConns, inMemory) {
        // Since these connections are on a brand new replset/cluster, this checks the propagation
        // of the initial setting of defaults.
        setDefaultsAndVerifyPropagation(setConn, checkConns, inMemory);

        // Do it again to check that updating the defaults also propagates correctly.
        setDefaultsAndVerifyPropagation(setConn, checkConns, inMemory);
    };

    /**
     * Asserts eventually all given nodes have no default RWC in their in-memory cache.
     */
    function verifyPropgationOfNoDefaults(checkConns) {
        assert.soon(() => checkConns.every(checkConn => {
            const defaultsRes = assert.commandWorked(
                checkConn.adminCommand({getDefaultRWConcern: 1, inMemory: true}));

            // Note localUpdateWallClockTime is generated by the in-memory cache, so it will be
            // present even if there are no defaults.
            const unexpectedFields = kDefaultRWCFields.concat(kExtraSetFields).filter(field => {
                return (field !== kDefaultWriteConcernField) && field !== kDefaultReadConcernField;
            });
            return unexpectedFields.every(field => !defaultsRes.hasOwnProperty(field));
        }),
                    "deleted/dropped defaults failed to propagate to all nodes within " +
                        timeoutSecs + " secs. checkConns: " + tojson(checkConns),
                    timeoutSecs * 1000,
                    intervalSecs * 1000,
                    {runHangAnalyzer: false});
    }

    /**
     * Tests that when the RWC defaults document is removed, either through a delete or drop of
     * config.settings, the RWC defaults cache is invalidated.
     */
    var runDropAndDeleteTests = function(mainConn, checkConns) {
        // Set the defaults to some value other than the implicit server defaults. Then remove the
        // defaults document and verify the cache is invalidated on all nodes.
        setDefaultsAndVerifyPropagation(mainConn, checkConns);
        assert.commandWorked(
            mainConn.getDB("config").settings.remove({_id: "ReadWriteConcernDefaults"}));
        verifyPropgationOfNoDefaults(checkConns);
    };

    return {runTests, runDropAndDeleteTests};
})();
